####
#
# Copyright 2007 B. Chandler Carruth
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
####
#
# ./lib/rcons/environment.rb
#
# The environment is a cornerstone of the entire system. It provides
# mechanisms for executing various actions. By constructing various
# environments, and handing off target chains to them to build, the
# behavior of various actions can be tailored for various purposes. Fore
# example, building a library constists of the same series of targets,
# whether building a debugging version or optimized release version.
#
#####

# Files required to implement this environment script
require( 'fileutils' )
require( 'rcons/target' )
require( 'rcons/tools/gcc/gcc_c_compiler_target' )

module RCons

  # The environment is the cornerstone of the RCons system.  It provides
  # mechanisms for executing various actions.  By constructing various
  # environments and handing off the chains to them to build, the behavior of
  # of various actions can be tailored for various purposes.  For example,
  # building a library consists of the same series of targets, whether
  # building a debugging version or optimized release version.
  #
  #   test_env = Environment.new( 'test environment', '.', 'build' )
  #
  # An +Environment+ object called +test_env+ will be created.  As
  # specified by the parameters, the environment's name will be 'test
  # environment', the source root directory will be the directory in which
  # the buildfile is located, and the build directory will be the
  # directory 'build/debug' relative to the buildfile.
  #
  # Extends the +Target+ class.
  class Environment < Target
    attr_reader( :path, :vars, :verbose )

    # Default initialization method called when class is generated
    # Constructor method to setup the environment when it is created.  Sets up
    # the dependencies of the environment, makes sure the paths specified
    # by +src_root+ and +build_root+ exist, and creates the scratch directory
    # possibly specified in +params+.
    #
    # Inputs:
    # +name+:: the name of the environment.
    # +src_root+:: the path to the root of the source directory structure.
    # +build_root+:: the path to the root of the build directory structure.
    # +params+:: optional parameters to the environment, such as initial environment variables.
    def initialize( name, src_root, build_root, params = {} )
      # Handle Configuration Target variables. NB: we do *not* use super,
      # because this target breaks rules: it clearly doesn't
      # depend on the build environment
      @env_name = name.to_s().dup()
      @name = ( Pathname.new('environment') + @env_name ).to_s()
      @brief = <<EOT
This is the "#{@env_name}" build environment's configuration target.
EOT
      @long = <<EOT
This is the configuration target for the "#{@env_name}" build environment. It is
used to manage rebuilding any targets which use the environment whenever some
aspect of it changes. This is target is very special, and breaks many rules for
targets in order to achieve its goals.
EOT
      @build_env = self
      @pre_deps = []
      @post_deps = []

      # Now Environment Stuff
      if params[ :vars ]
        @vars = params[ :vars ]
      else
        @vars = {}
      end
      @vars[ :ENV ] = @env_name

      # src root management
      self.src_root = src_root

      # build root management
      self.build_root = build_root

      # scratch root management, specific to each environment
      if params[ :scratch_root ]
        self.scratch_root = params[ :scratch_root ].to_s()
      else
        self.scratch_root = Pathname.new( '$(BUILD_ROOT)' ) + '.rcons' + '$(ENV)'
      end

      # Use specialized path, if we have one
      if params[ :path ]
        @path = params[ :path ]
      else
        @path = ENV[ 'PATH' ].split( ':' )
      end
      @path.compact!()
      @path.collect!() { |x| Pathname.new( x.to_s() ) }
      @path.reject!() { |x| !x.directory? }
      @path.uniq!()

      # Enable/Disable verbosity
      @verbose = 0
      if params[ :verbose ]
        @verbose = params[ :verbose ]
        unless @verbose.kind_of? Integer
          @verbose = @verbose.to_s().to_i()
        end
      end

      # Target builder registry
      @target_builders = {}
    end

    # Register a new target with this environment
    def register( builder )
      @target_builders[ builder.name.intern() ] = builder
    end

    # Lookup an existing target
    def lookup_builder( key )
      @target_builders[ key.intern() ]
    end

    # Expands variables in a string with the appropriate environment variables.
    # The syntax is to use the string +$(varName)+ in place of the
    # the environment variable +varName+.  Replacement is recursive, so if
    # any replaced string contains other variables to be replaced, they
    # will be replaced as well.  Note that this could lead to an infinite
    # loop.
    #
    # Inputs:
    # +str+:: the string for which to expand environment variables.
    #
    # <b>Return Value:</b> the original string with ocurrances of +$(varName)+ replaced with the data contained in the environment variable +varName+.
    def expand( str )
      return str.to_s().gsub( /\$\(([^()]+)\)/ ) { |m| expand( @vars[ $1.intern() ] ) }
    end

    # Expands variables in a string with the appropriate environment variables
    # and writes the expanded string to a file.
    # The syntax is to use the string +$(varName)+ in place of the
    # the environment variable +varName+.  Replacement is recursive, so if
    # any replaced string contains other variables to be replaced, they
    # will be replaced as well.  Note that this could lead to an infinite
    # loop.
    #
    # Inputs:
    # +str+:: the string for which to expand environment variables.
    # +dest_name+:: the path of the file in which to write the string.  Will be created if does not exist.
    def expand_file( src_name, dest_name )
      src_path = Pathname.new( expand( src_name ) )
      dest_path = Pathname.new( expand( dest_name ) )
      if src_path.readable?
        src = File.new( src_path, File::RDONLY )
        dest = File.new( dest_path, File::CREAT | File::TRUNC | File::WRONLY, src.stat().mode() )

        for line in src
          dest.puts( expand( src ) )
        end

        src.close()
        dest.close()
      end
    end


    # Special functions to retrieve values that are composed of
    # variables, and to manipulate those values, all the while keeping
    # them stored as variables

    # Returns the path to the root of the source directory structure.
    #
    # <b>Return Value</b> the path to the root source directory.
    def src_root
      expand( @vars[ :SRC_ROOT ] )
    end

    # Sets the path to the root of the source directory structure
    # dependant upon the parameter passed to the function.
    #
    # Inputs:
    # +p+:: the path to the root source directory.
    def src_root=( p )
      unless Pathname.new( expand( p ) ).directory?
        raise( ArgumentError,
          "Invalid source/ root argument: '#{p}' is not a directory.",
          caller() )
      end
      @vars[ :SRC_ROOT ] = p.to_s().dup()
    end

    # Returns the path to the root of the build directory structure.
    #
    # <b>Return Value:</b> the path to the root build directory.
    def build_root
      expand( @vars[ :BUILD_ROOT ] )
    end

    # Sets the path to the root of the build directory structure
    # dependent upon the parameter passed to the function.
    #
    # Inputs:
    # +p+:: the path to the root build directory.
    def build_root=( p )
      unless Pathname.new( expand( p ) ).directory?
        raise( ArgumentError,
          "Invalid build root argument: '#{p}' is not a directory.",
          caller() )
      end
      @vars[ :BUILD_ROOT ] = p.to_s().dup()
    end


    # Returns the path to the root of the scratch directory structure.
    #
    # <b>Return Value</b> the path to the root scratch directory.
    def scratch_root
      expand( @vars[ :SCRATCH_ROOT ] )
    end

    # Sets the path to the root of the scratch directory structure
    # dependent upon the parameter passed to the function.
    #
    # Inputs:
    # +p+:: the path to the root scratch directory
    def scratch_root=( p )
      @vars[ :SCRATCH_ROOT ] = p.to_s().dup()
    end

    # Provides a mechanism to abstract printing to the environment.  Prints
    # using the default verbosity.
    #
    # Inputs:
    # +args+:: array of strings to print.
    def print( *args )
      # Default verbosity output
      printv( 0, *args )
    end


    # Provides a mechanism to abstract printing to the environment
    # using a specified verbosity.
    #
    # Inputs:
    # +v+:: array of strings to print.
    # +args+:: array of strings to print.
    def printv( v, *args )
      # Print under certain verbosity
      if @verbose >= v
        puts( *args )
      end
    end

    # Search for an executable program. First checks to see if this
    # already is the path to an executable program, and then proceeds to
    # check the entire path for one. It returns a viable "command" to
    # execute. This has the added intelligence that it will _try_ to
    # simplify the command, if possible, for ease of reading. Ultimately
    # however, correctness is paramount.

    # Searches for an executable program, first in the current working
    # directory, and if not found in the entire path.
    #
    # Inputs:
    # +command+:: the name of the command to search for.
    #
    # <b>Return Value:</b> a string containing an command which can be
    # used to execute the command specified in the parameter +command+.
    def search_path( command )
      command = expand( command.to_s().strip() )
      if command =~ /\//
        # this appears to be an exact path, so check it
        cmd_path = Pathname.new( command )
        if cmd_path.executable?
          # It actually is a precise path, so use it!
          return command
        else
          # This is illegal, passing in a path, but not one to an
          # executable. We can't search the path, we have no recourse.
          return nil
        end
      else
        # We seem to have no directory components, so begin path search
        for path in @path
          cmd_path = path + command
          if cmd_path.executable?
            # Found an executable, check to see if its in the system
            # path so we can output "pretty" commands. This has to both
            # check that its in the system path, and that _this_ version
            # comes first in the system path.
            for sys_path in ENV[ 'PATH' ].split( ':' )
              sys_cmd_path = Pathname.new( sys_path ) + command
              if sys_cmd_path.executable?
                if sys_cmd_path.realpath == cmd_path.realpath
                  # We found the command, and its a match, use just the
                  # command!
                  return command
                else
                  # We found it in the path, but its not a match, so we
                  # can't use the path even if we find a match later...
                  break
                end
              end
            end

            # If we've been unable to use the system path, use a full
            # path to the executable to ensure correctness
            return cmd_path.cleanpath(true)
          end
        end

        # We found no command in the path! This command cannot be
        # executed!
        return nil
      end
    end

    # Evaluate the passed in command. If valid, execute.
    # Executes a command with specified arguments, first making
    # sure it is a valid command.
    #
    # Inputs:
    # +command+:: the name of the command to execute.  Doesn't have to be a full path.
    # +arguments+:: an array of strings containing each argument to pass to the executed command.
    def exec_command( command, arguments )
      # search the path for the command
      cmd_path = search_path( command )

      unless cmd_path
        $stderr.puts( "ERROR: Command not found: " + command )
        return false
      end

      # now add quotes to manage any whitespace...
      command = '"' + command + '"' if command =~ /\s/

      # now the arguments
      arguments.collect!() do |arg|
        arg = expand( arg.to_s().strip() )
        if arg =~ /\s/
          '"' + arg + '"'
        else
          arg
        end
      end

      # execute command
      cmd = "#{command} " + arguments.join( ' ' )
      IO.popen( cmd + ' 2>&1' ) do |p|
        if block_given?
          yield( cmd, p )
        end
      end

      return $?
    end

    # Prints out the environment including all environment variables
    # and the environment path.
    def to_s()
      out = "Environment Variables:\n"

      max_keylength = 0
      for key, val in @vars
        max_keylength = key.to_s().length() if key.to_s().length() > max_keylength
      end
      max_keylength += 3

      # now we can pretty print them
      for key, val in @vars
        out += sprintf( "  %#{max_keylength}s => '%s'\n", ( '$(' + key.to_s() + ')' ), val )
      end

      # now the path
      out += "\nEnvironment Path:\n"
      for p in @path
        out += "  '#{p}'\n"
      end
      out += "\n"

      return out
    end

    # Prepares the scratch area for the environment making sure the scratch
    # directory exists and if not creating it.
    def build()
      # Setup this environments scratch area
      scratch_root_path = Pathname.new( expand( self.scratch_root ) )

      unless scratch_root_path.directory?
        scratch_root_path.mkpath()
      end

      return true
    end
  end
end
